package org.iamd.util;

import static javax.faces.application.FacesMessage.SEVERITY_ERROR;
import static javax.faces.application.FacesMessage.SEVERITY_INFO;
import static javax.faces.context.FacesContext.getCurrentInstance;

import java.text.MessageFormat;
import java.util.Iterator;
import java.util.Locale;
import java.util.MissingResourceException;
import java.util.ResourceBundle;

import javax.faces.application.FacesMessage;
import javax.faces.application.FacesMessage.Severity;
import javax.faces.component.UIViewRoot;
import javax.faces.context.FacesContext;

import org.apache.log4j.Level;
import org.apache.log4j.Logger;
import org.iamd.conf.ConfigurationManager;
import org.iamd.domain.business.BusinessException;

/**
 * Клас, който затваря функционалност по управление на съобщенията.
 * 
 * @author И. Атанасов
 */
public class Messenger {

	/**
	 * Енумерация с типовете локали за интернационализация на съобщенията.
	 */
	public enum ApplicationLocales {

		/**
		 * Локал за администратора на системата
		 */
		SYSTEM_LOCALE {

			private Locale sysLocale;

			@Override
			public Locale getLocale() {
				if (this.sysLocale == null) {
					this.sysLocale = super.getLocale();
				}
				return this.sysLocale;
			}

		},

		/**
		 * Локал за краен потребител на системата
		 */
		USER_LOCALE {

			@Override
			public Locale getLocale() {
				FacesContext context = getCurrentInstance();
				if (context != null) {
					UIViewRoot viewRoot = context.getViewRoot();
					if (viewRoot != null) {
						return viewRoot.getLocale();
					}
				}
				return super.getLocale();
			}

		};

		/**
		 * @return ако има заложен в централната конфигурация параметър за локал - инстанцира него,
		 *         ако няма - локала на виртуалната машина
		 */
		public Locale getLocale() {
			String configuredLoc = ConfigurationManager.getInstance().getProperty(
					CommonConfigProps.DEFAULT_LOCALE);
			return configuredLoc != null ? new Locale(configuredLoc) : Locale.getDefault();
		}

	}

	/**
	 * Извлича съобщението от подаденото изключение и го добавя като faces message.
	 * 
	 * @param be
	 *            изключението
	 */
	public static void addException(BusinessException be) {
		getCurrentInstance().addMessage(null,
				new FacesMessage(SEVERITY_ERROR, be.getMessage(), be.getMessage()));
	}

	/**
	 * Добавя съобщение за грешка.
	 * 
	 * @param messageKey
	 *            ключа, под който се съдържа текста на грешката
	 */
	public static void addFacesErrMsg(IMsgKeys messageKey) {
		addFacesMessage(messageKey, null, SEVERITY_ERROR);
	}

	/**
	 * Добавя съобщение с информативен характер.
	 * 
	 * @param messageKey
	 *            ключа, под който се съдържа текста на съобщението
	 */
	public static void addFacesInfoMsg(IMsgKeys messageKey) {
		addFacesMessage(messageKey, null, SEVERITY_INFO);
	}

	/**
	 * Добавя съобщение с информативен характер.
	 * 
	 * @param messageKey
	 *            ключа, под който се съдържа текста на съобщението
	 * @param params
	 *            параметри за формиране на съобщението
	 */
	public static void addFacesInfoMsg(IMsgKeys messageKey, Object[] params) {
		addFacesMessage(messageKey, params, SEVERITY_INFO);
	}

	/**
	 * @return дали има съобщения за грешка в контекста
	 */
	public static boolean hasFacesErrorMessages() {
		FacesContext context = getCurrentInstance();
		if (context != null) {
			Iterator<FacesMessage> messages = context.getMessages();
			FacesMessage curr = null;
			while (messages.hasNext()) {
				curr = messages.next();
				if (curr.getSeverity().equals(FacesMessage.SEVERITY_ERROR)) {
					return true;
				}
			}
		}
		return false;
	}

	/**
	 * Създаване и логване на съобщения с приоритет DEBUG.
	 * 
	 * @param messageKey
	 *            ключа, под който се съдържа текста на съобщението
	 */
	public static void logDebug(IMsgKeys messageKey) {
		logDebug(messageKey, null);
	}

	/**
	 * Създаване и логване на съобщения с приоритет DEBUG.
	 * 
	 * @param messageKey
	 *            ключа, под който се съдържа текста на съобщението
	 * @param params
	 *            допълнителни параметри, с които се конструира текста на съобщението
	 */
	public static void logDebug(IMsgKeys messageKey, Object[] params) {
		logMessage(Level.DEBUG, messageKey, params, null);
	}

	/**
	 * Създаване и логване на съобщения с приоритет ERROR.
	 * 
	 * @param messageKey
	 *            ключа, под който се съдържа текста на съобщението
	 */
	public static void logError(IMsgKeys messageKey) {
		logError(messageKey, null, null);
	}

	/**
	 * Създаване и логване на съобщения с приоритет ERROR.
	 * 
	 * @param messageKey
	 *            ключа, под който се съдържа текста на съобщението
	 * @param params
	 *            допълнителни параметри, с които се конструира текста на съобщението
	 * @param t
	 *            пораждащо изключение
	 */
	public static void logError(IMsgKeys messageKey, Object[] params, Throwable t) {
		logMessage(Level.ERROR, messageKey, params, t);
	}

	/**
	 * Създаване и логване на съобщения с приоритет ERROR.
	 * 
	 * @param messageKey
	 *            ключа, под който се съдържа текста на съобщението
	 * @param t
	 *            пораждащо изключение
	 */
	public static void logError(IMsgKeys messageKey, Throwable t) {
		logError(messageKey, null, t);
	}

	/**
	 * Създаване и логване на съобщения с приоритет FATAL.
	 * 
	 * @param messageKey
	 *            ключа, под който се съдържа текста на съобщението
	 */
	public static void logFatal(IMsgKeys messageKey) {
		logFatal(messageKey, null, null);
	}

	/**
	 * Създаване и логване на съобщения с приоритет FATAL.
	 * 
	 * @param messageKey
	 *            ключа, под който се съдържа текста на съобщението
	 * @param params
	 *            допълнителни параметри, с които се конструира текста на съобщението
	 * @param t
	 *            пораждащо изключение
	 */
	public static void logFatal(IMsgKeys messageKey, Object[] params, Throwable t) {
		logMessage(Level.FATAL, messageKey, params, t);
	}

	/**
	 * Създаване и логване на съобщения с приоритет FATAL.
	 * 
	 * @param messageKey
	 *            ключа, под който се съдържа текста на съобщението
	 * @param t
	 *            пораждащо изключение
	 */
	public static void logFatal(IMsgKeys messageKey, Throwable t) {
		logFatal(messageKey, null, t);
	}

	/**
	 * Създаване и логване на съобщения с приоритет INFO.
	 * 
	 * @param messageKey
	 *            ключа, под който се съдържа текста на съобщението
	 */
	public static void logInfo(IMsgKeys messageKey) {
		logInfo(messageKey, null);
	}

	/**
	 * Създаване и логване на съобщения с приоритет INFO.
	 * 
	 * @param messageKey
	 *            ключа, под който се съдържа текста на съобщението
	 * @param params
	 *            допълнителни параметри, с които се конструира текста на съобщението
	 */
	public static void logInfo(IMsgKeys messageKey, Object[] params) {
		logMessage(Level.INFO, messageKey, params, null);
	}

	/**
	 * Създаване и логване на съобщения с приоритет TRACE.
	 * 
	 * @param messageKey
	 *            ключа, под който се съдържа текста на съобщението
	 */
	public static void logTrace(IMsgKeys messageKey) {
		logTrace(messageKey, null);
	}

	/**
	 * Създаване и логване на съобщения с приоритет TRACE.
	 * 
	 * @param messageKey
	 *            ключа, под който се съдържа текста на съобщението
	 * @param params
	 *            допълнителни параметри, с които се конструира текста на съобщението
	 */
	public static void logTrace(IMsgKeys messageKey, Object[] params) {
		logMessage(Level.TRACE, messageKey, params, null);
	}

	/**
	 * Създаване и логване на съобщения с приоритет WARN.
	 * 
	 * @param messageKey
	 *            ключа, под който се съдържа текста на съобщението
	 */
	public static void logWarn(IMsgKeys messageKey) {
		logWarn(messageKey, null);
	}

	/**
	 * Създаване и логване на съобщения с приоритет WARN.
	 * 
	 * @param messageKey
	 *            ключа, под който се съдържа текста на съобщението
	 * @param params
	 *            допълнителни параметри, с които се конструира текста на съобщението
	 */
	public static void logWarn(IMsgKeys messageKey, Object[] params) {
		logMessage(Level.WARN, messageKey, params, null);
	}

	/**
	 * Формиране на съобщение и добавянето му в текущия faces контекст.
	 * 
	 * @param messageKey
	 *            ключ на съобщението
	 * @param params
	 *            параметри на съобщението
	 * @param severity
	 *            тип на съобщението
	 */
	private static void addFacesMessage(IMsgKeys messageKey, Object[] params, Severity severity) {
		String msg = formMessage(messageKey, params, ApplicationLocales.USER_LOCALE);
		FacesContext context = getCurrentInstance();
		if (context != null) {
			context.addMessage(null, new FacesMessage(severity, msg, msg));
		}
	}

	/**
	 * По подаден бъндъл и ключ формира съобщение. Бъндъла се локализира на базата на подадения
	 * параметър. Ако той е <code>null</code> - търси се локал по конфигурационен за приложението
	 * параметър. Ако и той е <code>null</code> - инициализира се с локализацията на виртуалната
	 * машина. Методът хвърля IllegalArgumentException ако поне един параметрите
	 * <code><b>bundle</b></code> и <code><b>key</b></code> e <code>null</code>, ако бъндъла не може
	 * да бъде намерен, или ако ключа в бъндъла не може да бъде намерен.
	 * 
	 * @param key
	 *            ключа, под който се съдържа текста на съобщението
	 * @param params
	 *            допълнителни параметри, с които се конструира текста на съобщението
	 * @param locale
	 *            параметър за локализация
	 * 
	 * @return формираното съобщение
	 */
	public static String formMessage(IMsgKeys key, Object[] params, ApplicationLocales locale) {
		if (key == null) {
			throw new IllegalArgumentException("Message key cannot be null!");
		}
		String bundleName = key.getBundleName();
		if (bundleName == null) {
			throw new IllegalArgumentException("Message bundle cannot be null!");
		}
		ResourceBundle rb = null;
		try {
			ClassLoader loader = Thread.currentThread().getContextClassLoader();
			if (loader == null) {
				loader = key.getClass().getClassLoader();
			}
			rb = ResourceBundle.getBundle(bundleName, locale.getLocale(), loader);
		} catch (MissingResourceException e) {
			throw new IllegalArgumentException("Cannot find message bundle with name " + bundleName
					+ "!", e);
		}
		try {
			String msg = rb.getString(key.getMsgKey());
			return params != null ? MessageFormat.format(msg, params) : msg;
		} catch (MissingResourceException e) {
			throw new IllegalArgumentException("Cannot find key " + key + " in message bundle "
					+ bundleName + "!", e);
		}
	}

	/**
	 * Формиране на съобщение и логването му със съответния приоритет. Използва се логер, асоцииран
	 * със извикващия текущия метод клас.
	 * 
	 * @param level
	 *            приоритет на логване
	 * @param messageKey
	 *            ключа, под който се съдържа текста на съобщението
	 * @param params
	 *            допълнителни параметри, с които се конструира текста на съобщението
	 * @param t
	 *            пораждащо изключение
	 */
	private static void logMessage(Level level, IMsgKeys messageKey, Object[] params, Throwable t) {
		try {
			StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();
			Logger log = Logger.getLogger(Class.forName(stackTrace[3].getClassName()));
			String message = formMessage(messageKey, params, ApplicationLocales.SYSTEM_LOCALE);
			if (t != null) {
				log.log(level, message, t);
			} else {
				log.log(level, message);
			}
		} catch (ClassNotFoundException e) {
			throw new Error("Unexpected system behaviour!!!");
		}
	}

}